package com.zhang.zxx.boot.common.core.aop;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhang.zxx.boot.common.core.aspect.WebLogIgnoredPropertyAnnotationIntrospector;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;

/**
 * MvcLogAspect:MVC日志切面
 * 前置:主要打印API请求相关信息:ip 接口名 参数等
 * 后置:主要统计耗时
 * 异常通知:不做,留给全局异常处理
 * 环绕通知,最终通知:不做
 *
 * @author zhangxiaoxiang
 * @date 2021/12/05
 */
@Slf4j
@Aspect
@Component
@Profile({"dev", "prod", "test"})
public class MvcLogAspect {

    /**
     * 换行符f
     */
    private static final String LINE_SEPARATOR = System.lineSeparator();

    /**
     * 参数转JSON格式会用到
     */
    public static final ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = new ObjectMapper() {{
            setAnnotationIntrospector(new WebLogIgnoredPropertyAnnotationIntrospector());
        }};
        OBJECT_MAPPER.setLocale(Locale.CHINA);
        OBJECT_MAPPER.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
        OBJECT_MAPPER.setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN));
        OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * 以自定义 @MvcLog 注解为切点
     */
    @Pointcut(value = "@annotation(mvcLog)", argNames = "mvcLog")
    public void mvcLogPoint(MvcLog mvcLog) {
    }

    @Around(value = "mvcLogPoint(mvcLog)", argNames = "joinPoint,mvcLog")
    public Object doAround(ProceedingJoinPoint joinPoint, MvcLog mvcLog) throws Throwable {
        //前置----------------------------统计切入方法耗时
        // 开始打印请求日志 ApiOperation接口描述 RequestMapping接口路径
        ApiOperation apiOperation = null;
        RequestMapping requestMapping = null;

        try {
            String methodDescription;
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert attributes != null;

            Method currentMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
            Annotation[] methodAnnotations = currentMethod.getAnnotations();
            // 查找注解 接口文档的注解ApiOperation
            for (Annotation annotation : methodAnnotations) {
                apiOperation = annotation instanceof ApiOperation ? (ApiOperation) annotation : apiOperation;
                requestMapping = AnnotatedElementUtils.findMergedAnnotation(currentMethod, RequestMapping.class);
            }
            // 准备打印参数
            String logArgStr = this.getLogArgStr(mvcLog, joinPoint, attributes);
            // 判断是否为Controller层的方法
            if (Objects.nonNull(requestMapping)) {
                HttpServletRequest request = attributes.getRequest();
                // 填充操作信息
                if (Objects.nonNull(apiOperation)) {
                    // 绑定到RequestContextHolder中
                    attributes.setAttribute("apiOperation", apiOperation, RequestAttributes.SCOPE_REQUEST);
                    methodDescription = apiOperation.value();
                } else {
                    methodDescription = mvcLog.description();
                }
                //前置增强[都在环绕增强一并做了]
                this.printRequestBeforeLog(joinPoint, request, methodDescription, logArgStr, true);
            } else {
                methodDescription = mvcLog.description();

                this.printMethodBeforeLog(joinPoint, methodDescription, logArgStr, true);
            }
        } catch (Exception e) {
            log.warn("@mvcLog前置打印日志失败");
        }

        //后置----------------------------统计切入方法耗时
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        long consuming = endTime - startTime;

        try {
            String resultStr;
            if (result instanceof byte[]) {
                resultStr = "byte[" + ((byte[]) result).length + "]";
            } else {
                resultStr = OBJECT_MAPPER.writeValueAsString(result);
            }
            if (Objects.nonNull(requestMapping)) {
                //接口耗时
                this.printRequestAfterLog(consuming, resultStr, true);
            } else {
                //方法耗时
                this.printMethodAfterLog(consuming, resultStr, true);
            }
        } catch (Exception e) {
            log.warn("@mvcLog后置打印日志失败");
        }
        return result;
    }

    /**
     * 后置 打印接口耗时和相应数据
     *
     * @param consuming 耗时ms
     * @param resultStr 返回数据
     * @param printArg  打印标识 true表示打印
     */
    private void printRequestAfterLog(long consuming, String resultStr, boolean printArg) {
        log.info("【后置增强】接口耗时:{}ms", consuming);
        if (printArg && StrUtil.isNotBlank(resultStr)) {
            log.info("【后置增强】接口返回:{}", resultStr);
        }
        log.info("=========================================== END ===========================================");
    }

    /**
     * 后置 方法耗时
     *
     * @param consuming 耗时ms
     * @param resultStr 返回数据
     * @param printArg  打印标识
     */
    private void printMethodAfterLog(long consuming, String resultStr, boolean printArg) {
        log.info("【后置增强】方法耗时:{} ms", consuming);
        if (printArg) {
            log.info("【后置增强】方法返回:{}", resultStr);
        }
        log.info("========= END =========");
    }

    /**
     * 前置 方法方法
     *
     * @param joinPoint         joinPoint
     * @param methodDescription methodDescription
     * @param logArgStr         logArgStr
     * @param printArg          printArg
     */
    private void printMethodBeforeLog(JoinPoint joinPoint, String methodDescription, String logArgStr, boolean printArg) {
        log.info("======= METHOD =======");
        log.info("方法名称:{}#{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        log.info("{}", StrUtil.isNotBlank(methodDescription) ? "方法描述:" + methodDescription : null);
        log.info("{}", printArg && StrUtil.isNotBlank(logArgStr) ? "方法参数:" + logArgStr : null);
    }

    /**
     * 前置 控制层接口打印
     *
     * @param joinPoint         joinPoint
     * @param request           HttpServletRequest
     * @param methodDescription methodDescription
     * @param requestArgsStr    requestArgsStr
     * @param printArg          printArg
     */
    private void printRequestBeforeLog(JoinPoint joinPoint, HttpServletRequest request, String methodDescription, String requestArgsStr, boolean printArg) {
        // 打印请求相关参数 打印调用 controller 的全路径以及执行方法
        log.info("========================================== START ==========================================");
        if (StrUtil.isNotBlank(methodDescription)) {
            log.info("【前置增强】接口描述:{}", methodDescription);
        }
        log.info("【前置增强】接口路径[{}]:{}", request.getMethod(), request.getRequestURL().toString());
        log.info("【前置增强】接口类名:{}#{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        if (printArg && StrUtil.isNotBlank(requestArgsStr)) {
            log.info("【前置增强】请求参数:{}", requestArgsStr);
        }
        log.info("【前置增强】接口IP:{}", request.getRemoteAddr());

    }

    /**
     * 获取参数
     *
     * @param mvcLog    weblog注解
     * @param joinPoint joinPoint
     * @return 参数
     */
    @SneakyThrows
    private String getLogArgStr(MvcLog mvcLog, JoinPoint joinPoint, ServletRequestAttributes requestAttributes) {
        String[] params = mvcLog.params();
        //没有传指定参数默认打印全部参数,如果有RequestBody则打印JSON参数即可
        Object scanParamsArg = this.scanParamsArg(params, requestAttributes);
        Object requestBodyArg = this.scanRequestBodyArg(joinPoint);
        //优先判断[业务JSON传参情况多一些]
        if (Objects.nonNull(requestBodyArg)) {
            return OBJECT_MAPPER.writeValueAsString(requestBodyArg);
        }
        if (Objects.nonNull(scanParamsArg)) {
            return OBJECT_MAPPER.writeValueAsString(scanParamsArg);
        }
        return  null;
    }

    /**
     * <h3>扫描切点上与paramNames匹配的参数</h3>
     *
     * @param param 需要打印的参数[预留]
     * @return arg/null
     */
    private Object scanParamsArg(String[] param, ServletRequestAttributes requestAttributes) {
        Map<String, String[]> parameterMap = requestAttributes.getRequest().getParameterMap();
        if (Objects.isNull(parameterMap)) {
            return null;
        }
        JSONObject jsonObject = new JSONObject();
        for (String s : parameterMap.keySet()) {
            if (param.length < 1) {
                jsonObject.putOpt(s, parameterMap.get(s));
            } else {
                for (String value : param) {
                    jsonObject.putOpt(value, parameterMap.get(value));
                }
            }
        }
        if (jsonObject.keySet().size()>0) {
            log.warn("@RequestParam参数转换成JSON:{}", jsonObject);
        }
        return jsonObject;
    }

    /**
     * <h3>扫描切点上带RequestBody注解的参数</h3>
     *
     * @param joinPoint 切点
     * @return arg/null
     */
    @Nullable
    private Object scanRequestBodyArg(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Method currentMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Annotation[][] parameterAnnotations = currentMethod.getParameterAnnotations();
        int argNum;
        for (argNum = 0; argNum < parameterAnnotations.length; argNum++) {
            for (Annotation a : parameterAnnotations[argNum]) {
                if (a instanceof RequestBody) {
                    return args[argNum];
                }
            }
        }
        return null;
    }
}
